package de.bk.timecalc.core;

/**
 * A simply stop watch. The Stop Watch is in one of three states: stopped,
 * running or paused. The inital state is stopped.
 * 
 * @author Bastian Krol
 * @version $Revision:$ $Date:$ $Author:$
 */
public class StopWatch
{
  private enum State
  {
    STOPPED, RUNNING, PAUSED;
  }

  private State state;

  private long startMillis;
  private long millisUntilPaused;

  public StopWatch()
  {
    this.state = State.STOPPED;
  }

  /**
   * Starts the stop watch. It may not be running already (that is, it's state
   * has to be either stopped or paused), otherwise an IllegalStateException is
   * thrown. Afterwards, the state is running.
   * 
   * @throws IllegalStateException
   *           if the stop watch has been started (and not been stopped) before.
   */
  public void start()
  {
    switch (this.state)
    {
      case RUNNING:
      {
        throw new IllegalStateException(
            "StopWatch#start() has already been called. Call StopWatch#stop() or StopWatch#pause() before calling StopWatch#start() again, or use StopWatch#restart().");
      }
      case STOPPED:
      {
        this.startMillis = System.currentTimeMillis();
        break;
      }
      case PAUSED:
      {
        this.startMillis = System.currentTimeMillis() - this.millisUntilPaused;
        this.millisUntilPaused = 0;
        break;
      }
      default:
      {
        throw new RuntimeException("Unknown state: " + this.state);
      }
    }
    this.state = State.RUNNING;
  }

  /**
   * Stops the stop watch and returns the time. Must have been started before
   * (that is, the stop watch must have the state running or paused), otherwise
   * an IllegalStateException is thrown. Afterwards, the state is stopped. May
   * be started again afterwards.
   * 
   * @return the Duration since {@link #start()} has been called
   * @throws IllegalStateException
   *           if the stop watch has not been started before (or has been
   *           started and stopped again).
   */
  public Duration stop()
  {
    switch (this.state)
    {
      case STOPPED:
      {
        throw new IllegalStateException(
            "StopWatch#start() has to be called before StopWatch#stop() is called.");
      }
      case RUNNING:
        // falls through
      case PAUSED:
      {
        Duration duration = new Duration(this.getMillisRun());
        this.state = State.STOPPED;
        return duration;
      }
      default:
      {
        throw new RuntimeException("Unknown state: " + this.state);
      }
    }
  }

  /**
   * Pauses the stop watch. Must have been started before (that is the stop
   * watch has to be in state running or paused), otherwise an
   * IllegalStateException is thrown. If the stop watch is already in state
   * pause, calling pause again has no effect. The stop watch can be started
   * again with {@link #start()}.
   * 
   * @throws IllegalStateException
   *           if the stop watch has not been started before (or has been
   *           started and stopped again).
   */
  public void pause()
  {
    switch (this.state)
    {
      case STOPPED:
      {
        throw new IllegalStateException(
            "StopWatch#start() has to be called before StopWatch#pause() is called.");
      }
      case RUNNING:
      {
        this.millisUntilPaused = this.getMillisRun();
        this.state = State.PAUSED;
        break;
      }
      case PAUSED:
      {
        // nothing to do
        break;
      }
      default:
      {
        throw new RuntimeException("Unknown state: " + this.state);
      }
    }
  }

  /**
   * Stops the stop watch, returns the time and starts the stop watch again
   * immediately. Must have been started before, otherwise an
   * IllegalStateException is thrown.
   * 
   * @return the Duration since {@link #start()} has been called
   * @throws IllegalStateException
   *           if the stop watch has not been started before (or has been
   *           started and stopped again).
   */
  public Duration restart()
  {
    Duration duration = this.stop();
    this.start();
    return duration;
  }

  private long getMillisRun()
  {
    return System.currentTimeMillis() - this.startMillis;
  }
}
